logo头像

学如逆水行舟

iOS研习记——聊聊野指针与僵尸对象定位

[iOS研习记]——聊聊野指针与僵尸对象定位

一、从一个异常说起

在iOS项目开发中,或多或少的我们都会遇到一些Crash的情况,大部分Crash抛出的异常都是NSException层的,这类异常是OC层代码问题造成的,通常堆栈信息和异常的提示信息都非常明确,可以直接定位到出问题的代码,从而使这类问题的解决并不困难。可以引起Crash的异常除了NSException外,还有Unix层和Mach层的异常。

Mach异常一般是底层内核级的异常,我们可以通过一些底层的API来对这类异常进行捕获,这不是本文的讨论内容,这里不再赘述。Unix层是指对于开发者没有捕获的Mach异常,会被转换成对应的Unix信号传递给出错线程。

如果你在iOS项目在线上收集的异常中,有类似EXC_BAD_ACCESS的异常,则大概率是由于内存问题产生的野指针引起的。这也是本文我们要讨论的核心内容。

1. 什么是野指针?

当前我们在编写iOS程序时大多会采用ARC来进行内存管理,通常情况下我们无需过多的对内存管理进行关心。但是这并不代表不会产生内存问题。从原理上讲,我们在创建任何对象的时候,首先都会通过操作系统从内存中申请出一块内存空间供此对象使用,并将此内存空间的地址保存到指针中供我们在代码中方便的引用到此内存。那么当这个对象被销毁的时候,原则上我们需要做两件事,一是将这块内存还回去,之后操作系统可以重复利用这块内存分配给其他申请者使用,二是将代码中的指针清空回收。这样可以保证程序能够可持续化的健康运行。工作过程如下图所示:

但是无论在生活中还是编程中,意外总会发生,通常情况下,在向操作系统申请内存这一步很少会出现问题,操作系统本身的稳定性比应用程序要强很多。问题大多出现在内存释放的时候。问题可能有两种:

一种是已经不需要使用的对象我们将指针变量直接清除了,但却没有告诉操作系统回收这块内存,此后程序中没有地方存储这块内存的地址,这块内存将永远无法使用和回收。这种情况下,这块内存就变成了无主内存且操作系统并不知道,就产生了我们常说的内存泄露问题,随着应用的运行时间越来越长,内存泄露可能越来越多最终导致内存不够用,程序无法再正常运行。

另一种是我们告诉操作系统要回收这块内存,并且这块内存也真正的被回收了,但是程序中依然有指针变量存储着这个地址没有清空,此时这个指针就变成了也指针,因为它所指向的内存已经回收,这块内存具体是又被利用了还是依然存放着原来的数据我们都一无所知。此后如果不小心又通过这个指针使用了这块内存的数据,无论读写都将产生各种千奇百怪的问题,且我们很难定位。本文我们主要聊的就是这类野指针问题的产生原因与定位方法。

2. 野指针会产生哪些问题?

开发中我们遇到的大部分的EXC_BAD_ACCESS问题都是由野指针导致的,主要有两种信号:SIGSEGV和SIGBUS。其中SIGSEGV表示操作的地址非法,访问了未分配的内存或者写入了没有写权限的内存。SIGBUS表示错误的内存类型访问。

野指针会产生的问题千奇百怪,难以定位。当程序中使用到了野指针时,可能存在两大种场景:

1> 访问的内存没有被覆盖

如果原对象依赖的其他对象没有被删除,则看上去程序的运行好像任何问题,但是实际上却很危险,程序逻辑上的表现已经不可控。

如果原对象依赖的其他对象有删除情况,则内部可能还有有其他野指针生成,依然会出现各种复杂的异常场景。

2> 访问的内存重新被覆盖了

这种从场景会更加麻烦,如果当前内存区域的可访问性发生了变化,则会产生许多类型的异常,例如objc_msgSend失败,SIGBUS地址类型异常,SIGFPE运算符异常,SIGILL指令异常等等。

如果当前内存是可以访问的,则可能违背我们本意的写坏其他地方在使用的内存,使其他地方在使用时产生异常。也可能要使用的数据类型和我们原对象对不上,导致未实现的选择器类的错误,查找方法类的错误,各种底层逻辑错误,以及malloc错误等。这时要排查问题就非常难了。

综上所述,野指针的危害是非常大,除了其本身会造成异常Crash外,还可能会使其他正常使用的代码产生异常,并且有不可复现性与随机性,例如你可能发现某个Crash的堆栈是调用了某个对象的某个方法找不大,但是你搜遍代码也没有找到类似的方法调用,其实就是其他地方出现了野指针问题,之后这个正确的对象刚好分配到了野指针所指向的内存,野指针将此内存的数据破坏了。对于这种Crash问题,我们几乎是束手无策的。

3. 动手造一个野指针场景试试看

通过前面的介绍,我们了解了野指针问题的产生原因与危害。现在可以动手一试。使用Xcode新建一个iOS工程。在其中新建一个名为MyObject的类,为其添加一个属性,如下:

1
2
3
4
5
6
7
#import <Foundation/Foundation.h>

@interface MyObject : NSObject

@property(copy) NSString *name;

@end

在ViewController类中编写如下测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#import "ViewController.h"
#import "MyObject.h"
@interface ViewController ()

@property (nonatomic, unsafe_unretained)MyObject *object;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
MyObject *object = [[MyObject alloc] init];
self.object = object;
self.object.name = @"HelloWorld";
void *p = (__bridge void *)(self.object);
NSLog(@"%p,%@",self.object,self.object.name);
NSLog(@"%p,%@",p, [(__bridge MyObject *)p name]);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%p",self->_object);
NSLog(@"%@",self.object.name);
}

@end

这里我们手动的造出了一个会出现野指针问题的场景,ViewController类的object属性声明为的unsafe_unretained,这个修饰符的意思是当前属性不被ARC所管理,其所引用的对象释放后,此指针也不会被置空。上面代码我们在viewDidLoad方法中创建了一个MyObject对象,并复制给了当前控制器的object属性,由于栈内对象的生命周期为当前代码块内有效,因此当viewDidLoad方法结束后,此内存就会被回收,此时object指针就成了野指针。

我们可以在viewDidLoad方法的最后打上断点,观察当前MyOject对象的内存分配地址,如下:

可以看到,当次运行时,object对象分配的内存地址为0x600001e542d0(每次运行都会不同),后面访问对象的属性实际上就是对此内存中数据的访问,我们如果知道了内存地址,也可以直接使用地址进行访问,不一定要有变量,例如上图中通过LLDB中的po指令可以直接向内存地址发送消息,效果和通过变量调用对象方法是一样的。

之后,我们可以在运行后点击一下当前页面,大部分情况下都会出现地址异常Crash,我们可以通过LLDB输出下线程的堆栈信息,如下:

还有时候,程序可能会直接Crash到main方法中,输入更奇怪的堆栈信息,如下:

如上图所示,堆栈信息提示我们调用了数组的name方法,这其实就是因为此块内存被重新分配了。

我们只创建了一个没有任何逻辑的Demo项目,野指针的问题都如此多样,如果是在实际项目中,出了野指针问题我们更难找到问题源头。并且在ARC环境下,上面示例的场景其实很好排查到,更多产生野指针的原因是多线程不安全的读写数据造成的,结合多线程使用,野指针的问题则更加难查。

二、从原理看野指针的监控

要解决由野指针产生的问题,除了编程时尽量注意一些,避免危险的写法外。更重要的是能总结出一套方案来流程化的对此类问题进行监控。由于野指针问题的特性所致,我们在内存释放时其实是并不知道是否会产生野指针问题的,发生了野指针问题后也无法回溯。因此我们要用预设的思路来找这类问题的监控方案,即假设当前内存释放后依然有野指针要访问它,在设计时,我们可以不真正的将这块内存释放,而是将这块内存标记成有问题的,之后如果又发现有对这块有问题内存的访问出现,则表明出现了野指针问题。在标记内存时,我们也可以记录一下原对象的某些信息,例如类名,这样在发生野指针问题时,无论具体Crash的堆栈情况如何,我们都可以知道是具体哪个类的对象释放问题产生的野指针,能极大的缩小问题的排查范围。

因此处理野指针问题的核心点在于两点:

1.预设标记内存,被动等待野指针问题触发。

2.记录产生野指针问题的类,从类对象的使用入手排查而不是Crash时的堆栈入手排查。

针对如上两点,我们来看下如何实现。

1. 僵尸对象

将要释放的对象内存不真正回收,而是仅仅进行标记,我们会形象的成此时的对象为“僵尸对象”。Xcode默认支持开启僵尸对象,当我们向一个僵尸对象进行访问时,就会必然产生Crash,并且控制台输出相关提示信息。在Xcode中对要运行的scheme进行编辑,打开僵尸对象功能,如下所示:

再次运行工程,程序Crash后将输出如下信息:

1
*** -[MyObject retain]: message sent to deallocated instance 0x600000670670

我们可以明确的知道是MyObject对象的内存问题导致了野指针崩溃。

Xcode的僵尸对象功能虽然好用,但是只能调试时使用,更多时候我们产生的野指针问题都是线上环境的,而且无法复现,这个功能就显得非常鸡肋的。我们能否不依赖Xcode来实现野指针的监控呢?首先我们需要先搞明白Xcode中僵尸对象的实现原理。

2. Apple僵尸对象的实现原理探究

首先我们大致可以知道,要实现僵尸对象大概率是要对dealloc方法做些事情的,我们可以从这个方法入手找线索,查看objc的源代码,在其NSObject.m中可以看到如下代码:

1
2
3
4
// Replaced by NSZombies
- (void)dealloc {
_objc_rootDealloc(self);
}

从注释可以看到,系统实现的僵尸对象的确是处理dealloc方法了,推测其实通过Runtime替换了NSObject的dealloc方法。在CoreFoundation的源码中也有部分关于Zombies的内容,在CFRuntime.c中可以看到如下代码:

1
2
3
4
extern void __CFZombifyNSObject(void);  // from NSObject.m

void _CFEnableZombies(void) {
}

其中,_CFEnableZombies比较好理解,它应该是来表示是否开启僵尸对象功能的,应该和我们在Xcode中设置的环境变量功能一致,__CFZombifyNSObject从注释可以知道,应该是对僵尸对象的实现。我们在Xcode中添加一个__CFZombifyNSObject的符号断点,断点后内容如下所示:

看到这里的汇编,你应该不会太陌生,我们把核心的伪代码提出来,大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义字符串
define "NSObject"
// 用来获取NSObject类
objc_lookUpClass "NSObject"
// 定义字符串
define "dealloc"
define "__dealloc_zombie"
// 获取dealloc方法的实现
class_getInstanceMethod "NSObject" "dealloc"
// 获取__dealloc_zombie方法的实现
class_getInstanceMethod "NSObject" "__dealloc_zombie"
// 交换dealloc与__dealloc_zombie的方法实现
method_exchangeImplementations "dealloc" "__dealloc_zombie"

和我们想的差不多,下面我们可以再添加一个__dealloc_zombie的符号断点,看一看__dealloc_zombie方法是怎么实现的,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
CoreFoundation`-[NSObject(NSObject) __dealloc_zombie]:
-> 0x10ef77c49 <+0>: pushq %rbp
0x10ef77c4a <+1>: movq %rsp, %rbp
0x10ef77c4d <+4>: pushq %r14
0x10ef77c4f <+6>: pushq %rbx
0x10ef77c50 <+7>: subq $0x10, %rsp
0x10ef77c54 <+11>: movq 0x2e04fd(%rip), %rax ; (void *)0x0000000110021970: __stack_chk_guard
0x10ef77c5b <+18>: movq (%rax), %rax
0x10ef77c5e <+21>: movq %rax, -0x18(%rbp)
0x10ef77c62 <+25>: testq %rdi, %rdi
0x10ef77c65 <+28>: js 0x10ef77d04 ; <+187>
0x10ef77c6b <+34>: movq %rdi, %rbx
0x10ef77c6e <+37>: cmpb $0x0, 0x488703(%rip) ; __CFConstantStringClassReferencePtr + 7
0x10ef77c75 <+44>: je 0x10ef77d1d ; <+212>
0x10ef77c7b <+50>: movq %rbx, %rdi
0x10ef77c7e <+53>: callq 0x10eff4b52 ; symbol stub for: object_getClass
0x10ef77c83 <+58>: leaq -0x20(%rbp), %r14
0x10ef77c87 <+62>: movq $0x0, (%r14)
0x10ef77c8e <+69>: movq %rax, %rdi
0x10ef77c91 <+72>: callq 0x10eff464e ; symbol stub for: class_getName
0x10ef77c96 <+77>: leaq 0x242db5(%rip), %rsi ; "_NSZombie_%s"
0x10ef77c9d <+84>: movq %r14, %rdi
0x10ef77ca0 <+87>: movq %rax, %rdx
0x10ef77ca3 <+90>: xorl %eax, %eax
0x10ef77ca5 <+92>: callq 0x10eff4570 ; symbol stub for: asprintf
0x10ef77caa <+97>: movq (%r14), %rdi
0x10ef77cad <+100>: callq 0x10eff4ab0 ; symbol stub for: objc_lookUpClass
0x10ef77cb2 <+105>: movq %rax, %r14
0x10ef77cb5 <+108>: testq %rax, %rax
0x10ef77cb8 <+111>: jne 0x10ef77cd7 ; <+142>
0x10ef77cba <+113>: leaq 0x2427aa(%rip), %rdi ; "_NSZombie_"
0x10ef77cc1 <+120>: callq 0x10eff4ab0 ; symbol stub for: objc_lookUpClass
0x10ef77cc6 <+125>: movq -0x20(%rbp), %rsi
0x10ef77cca <+129>: movq %rax, %rdi
0x10ef77ccd <+132>: xorl %edx, %edx
0x10ef77ccf <+134>: callq 0x10eff4a62 ; symbol stub for: objc_duplicateClass
0x10ef77cd4 <+139>: movq %rax, %r14
0x10ef77cd7 <+142>: movq -0x20(%rbp), %rdi
0x10ef77cdb <+146>: callq 0x10eff482e ; symbol stub for: free
0x10ef77ce0 <+151>: movq %rbx, %rdi
0x10ef77ce3 <+154>: callq 0x10eff4a5c ; symbol stub for: objc_destructInstance
0x10ef77ce8 <+159>: movq %rbx, %rdi
0x10ef77ceb <+162>: movq %r14, %rsi
0x10ef77cee <+165>: callq 0x10eff4b6a ; symbol stub for: object_setClass
0x10ef77cf3 <+170>: cmpb $0x0, 0x48867f(%rip) ; __CFZombieEnabled
0x10ef77cfa <+177>: je 0x10ef77d04 ; <+187>
0x10ef77cfc <+179>: movq %rbx, %rdi
0x10ef77cff <+182>: callq 0x10eff482e ; symbol stub for: free
0x10ef77d04 <+187>: movq 0x2e044d(%rip), %rax ; (void *)0x0000000110021970: __stack_chk_guard
0x10ef77d0b <+194>: movq (%rax), %rax
0x10ef77d0e <+197>: cmpq -0x18(%rbp), %rax
0x10ef77d12 <+201>: jne 0x10ef77d3d ; <+244>
0x10ef77d14 <+203>: addq $0x10, %rsp
0x10ef77d18 <+207>: popq %rbx
0x10ef77d19 <+208>: popq %r14
0x10ef77d1b <+210>: popq %rbp
0x10ef77d1c <+211>: retq
0x10ef77d1d <+212>: movq 0x2e0434(%rip), %rax ; (void *)0x0000000110021970: __stack_chk_guard
0x10ef77d24 <+219>: movq (%rax), %rax
0x10ef77d27 <+222>: cmpq -0x18(%rbp), %rax
0x10ef77d2b <+226>: jne 0x10ef77d3d ; <+244>
0x10ef77d2d <+228>: movq %rbx, %rdi
0x10ef77d30 <+231>: addq $0x10, %rsp
0x10ef77d34 <+235>: popq %rbx
0x10ef77d35 <+236>: popq %r14
0x10ef77d37 <+238>: popq %rbp
0x10ef77d38 <+239>: jmp 0x10eff44c8 ; symbol stub for: _objc_rootDealloc
0x10ef77d3d <+244>: callq 0x10eff443e ; symbol stub for: __stack_chk_fail

汇编内容较多,整体流程是比较清晰的,伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 获取当前类
object_getClass
// 通过当前类获取当前类型
class_getName
// 将_NSZombie_拼接上当前类名
zombiesClsName = "_NSZombie_%s" + className
// 获取zombiesClsName类
objc_lookUpClass zombiesClsName
// 判断是否已经存在zombiesCls
if not zombiesCls:
// 如果不存在
// 现获取"_NSZombie_"类
cls = objc_lookUpClass "_NSZombie_"
// 复制出一个cls类,类名为zombiesClsName
objc_duplicateClass cls zombiesClsName
// 字符串变量释放
free zombiesClsName
// objc中原本的对象销毁方法
objc_destructInstance(self)
// 将当前对象的类修改为zombiesCls
object_setClass zombiesCls
// 判断是否开启了僵尸对象功能
if not __CFZombieEnabled:
// 如果没开启 将当前内存释放掉
free

上面的伪代码基本是__dealloc_zombie方法实现的整体过程,在objc源码中,NSObject类原本的dealloc方法实现路径如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
- (void)dealloc {
_objc_rootDealloc(self);
}

void _objc_rootDealloc(id obj)
{
ASSERT(obj);
obj->rootDealloc();
}

inline void objc_object::rootDealloc()
{
// taggedPointer无需回收内存
if (isTaggedPointer()) return; // fixme necessary?
// nonpointer为1表示不只是地址,isa中包含了其他信息
// weakly_referenced表示是否有弱引用
// has_assoc 表示是否有关联属性
// has_cxx_dtor 是否需要C++或Objc析构
// has_sidetable_rc是否有散列表计数引脚
if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
// 如果都没有 直接回收内存
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
id object_dispose(id obj)
{
if (!obj) return nil;
// 进行内存回收前的销毁工作
objc_destructInstance(obj);
free(obj);
return nil;
}

可以看到,__dealloc_zombie与真正的dealloc的实现其实只差了当前内存的回收部分,objc_destructInstance方法会正常执行的,这个方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void *objc_destructInstance(id obj) 
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// C++ 析构
if (cxx) object_cxxDestruct(obj);
// 移除关联属性
if (assoc) _object_remove_assocations(obj);
// 弱引用表和散列表的清除
obj->clearDeallocating();
}

return obj;
}

通过上面的分析,我们发现,其实系统实现的僵尸对象非常安全,并不对正常代码的运行产生负面作用,唯一的影响在于内存不回收会增加内存的使用负担,但是可以通过某些策略来进行释放。

三、手动实现线上野指针问题收集

理解了系统僵尸对象的实现原理,即是不依赖Debug环境,我们也可以仿照此思路来实现僵尸对象监控功能。

1. 仿照Apple的僵尸对象思路实现

首先创建一个名为_YHZombie_的模板类,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// _YHZombie_.h
#import <Foundation/Foundation.h>

@interface _YHZombie_ : NSObject

@end


// _YHZombie_.m
#import "_YHZombie_.h"

@implementation _YHZombie_

// 调用这个对象对的所有方法都hook住进行LOG
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%p-[%@ %@]:%@",self ,[NSStringFromClass(self.class) componentsSeparatedByString:@"_YHZombie_"].lastObject, NSStringFromSelector(aSelector), @"向已经dealloc的对象发送了消息");
// 结束当前线程
abort();
}

@end

在新建一个NSObject的类别,用来替换dealloc方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
//  NSObject+YHZombiesNSObject.h
#import <Foundation/Foundation.h>

@interface NSObject (YHZombiesNSObject)

@end


// NSObject+YHZombiesNSObject.m
#import "NSObject+YHZombiesNSObject.h"
#import <objc/objc.h>
#import <objc/runtime.h>

@implementation NSObject (YHZombiesNSObject)

+(void)load {
[self __YHZobiesObject];
}

+ (void)__YHZobiesObject {
char *clsChars = "NSObject";
Class cls = objc_lookUpClass(clsChars);
Method oriMethod = class_getInstanceMethod(cls, NSSelectorFromString(@"dealloc"));
Method newMethod = class_getInstanceMethod(cls, NSSelectorFromString(@"__YHDealloc_zombie"));
method_exchangeImplementations(oriMethod, newMethod);

}

- (void)__YHDealloc_zombie {
const char *className = object_getClassName(self);
char *zombieClassName = NULL;
asprintf(&zombieClassName, "_YHZombie_%s", className);
Class zombieClass = objc_getClass(zombieClassName);
if (zombieClass == Nil) {
zombieClass = objc_duplicateClass(objc_getClass("_YHZombie_"), zombieClassName, 0);
}
objc_destructInstance(self);
object_setClass(self, zombieClass);
if (zombieClassName != NULL)
{
free(zombieClassName);
}
}


@end

上面代码,除了一些容错判断没有加之外,思路和系统的僵尸对象一模一样。

再次运行我们的测试代码,在访问到野指针的时候将百分百的产生异常中断,并输出如下:

1
0x600003a8c2e0-[MyObject name]:向已经dealloc的对象发送了消息

现在,我们已经从原理上简单实现了一个不依赖于Xcode的野指针监控工具。

2. 将监控推广到C指针

通过对象的僵尸化,对OC层的野指针问题可以做到很好的监控作用,但是这种方法并不实用与C层的指针,项目中如果用到C相关的指针,由于内存的管理方式不走引用计数,无法通过Hook dealloc的方式来僵尸化对象。例如,我们创建一个如下的结构体:

1
2
3
typedef struct {
NSString *name;
} MyStruct;

在使用此结构体时,如果初始化之前进行了使用或内存回收后进行了使用都可能会出现野指针问题,如下:

1
2
3
4
5
6
7
8
9
10
11
12
MyStruct *p;
p = malloc(sizeof(MyStruct));
// 此时内存中的数据不可控 可能是之前未擦除的
printf("%x\n", *((int *)p));
// 使用可能会出现野指针问题
NSLog(@"%@", p->name);
// 进行内存数据的初始化
p->name = @"HelloWorld";
// 回收内存
free(p);
// 此时内存中的数据不可控
NSLog(@"%@", p->name);

我们可以思考下,出现上面野指针场景的主要原因是:

1. 获取到分配的内存后,如果此内存之前有过使用,数据此时是不可控的,当前指针直接使用此数据会有问题。

2.回收内存后,当前内存中的数据是不可控的,可能有别人或之前未清除的指针使用到。

无论是上面哪种场景,此野指针问题都有非常大的随机性,难以调试。因此我们核心要处理的地方在于把随机性改为必然性,即想办法让使用到这些有问题的内存时直接Crash,而不是可能Crash。要处理场景1很容易,我们可以hook住C中的malloc方法,分配了内存后直接将一个约定好的异常数据写入内存,这样在初始化之前使用到此数据时必然产生Crash。对于场景2,我们可以hook住C中的free方法,回收内存后将一个约定好的异常数据直接写入此内存,下次如果此内存没有被再分配,使用到它后也必然产生Crash。Xcode提供的Malloc Scribble调试功能,就是用这种思路实现的。

开启Xcode的Malloc Scribble选项,运行上面代码,效果如下图所示:

可以看到,在malloc分配内存之后,所有字节都被填入了0xAA,未初始化前使用就会必然产生Crash。这与Apple官方文档的解释是一致的,但是在free之后,内存数据获取到的可能并不是文档所说的0x55,是因为这块内存可能被其他内容覆写了。官网文档描述如下:

我们也可以手动根据Malloc Scribble的思路来实现一个将野指针问题从随机变成必然的工具,只需要重写系统的malloc相关的函数与free函数即可。对于C语言函数的Hook,我们可以直接使用fishhook库:

https://github.com/facebook/fishhook

导入上面库后,新建一个命名为YHMallocScrbble的类,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//  YHMallcScrbble.h
#import <Foundation/Foundation.h>

@interface YHMallcScrbble : NSObject

@end

// YHMallcScrbble.m
#import "YHMallcScrbble.h"
#import "fishhook.h"
#import "malloc/malloc.h"


void * (*orig_malloc)(size_t __size);
void (*orig_free)(void * p);


void *_YHMalloc_(size_t __size) {
void *p = orig_malloc(__size);
memset(p, 0xAA, __size);
return p;
}

void _YHFree_(void * p) {
size_t size = malloc_size(p);
memset(p, 0x55, size);
orig_free(p);
}



@implementation YHMallcScrbble

+ (void)load {
rebind_symbols((struct rebinding[2]){{"malloc", _YHMalloc_, (void *)&orig_malloc}, {"free", _YHFree_, (void *)&orig_free}}, 2);
}

@end

这样我们就实现了将野指针问题从随机变成必然,并且通用C指针。

相比僵尸对象方案,Malloc Scribble方法可以通用C指针,并且真正实现了对对象内存的回收,不会暂用内存。但是也有很大的弊端,比如对于free后写入的0x55在很多情况下都是无效的,因为这块内存可能又被其他地方改写了,导致Crash依然是随机的。当然我们也可以在自定义的free方法中不调用原系统的free,使得这块内存强制不能分配出去,这样其实和僵尸对象方案就比较类似了。并且相对僵尸对象方案,Malloc Scribble只能一定程度上将随机变成必然,方便问题的暴露,但是对开发者来说,并没有太多的信息告诉我们具体是什么类型的数据出的问题,排查还是有难度。

四、一些扩展

上面只是简单介绍了对野指针问题监控的一些手段原理。除了僵尸对象和Malloc Scribble外,Xcode中还提供了Address Sanitizer工具来做内存问题的监控,其原理也是对malloc和free函数做了处理,但程序访问到了有问题的内存时可以及时Crash,同时这个工具可以将对象在malloc时的堆栈信息进行存储,方面我们定位问题。无论采用哪种方法,如果我们真的要在线上执行,要做的事情其实还有很多,例如数据的收集策略,僵尸对象内存的清理时机,何时判断出有问题并抓取堆栈等等。

最后,希望本文可以为你对开发中野指针问题的处理带来一些思路。本文中所编写的示例代码可以在如下地址下载:

https://github.com/ZYHshao/ZombiesDemo

专注技术,懂的热爱,愿意分享,做个朋友

QQ:316045346